/*-------------------------------------------------------------------------
 *
 * nbtsort.c
 *		通过顺序加载叶页面从排序输入构建一个 btree。
 *
 * NOTES
 *
 * 我们使用 tuplesort.c 将给定的索引元组排序。然后我们按顺序扫描索引元组并为每个级别构建 btree 页面。我们将源元组加载到叶级页面中。每当我们在一个级别填满一个页面时，我们会将其链接添加到其父级（如有必要启动新的父级）。完成后，我们将每个最终页面写出到每个级别，将其添加到其父级。当我们在一个级别上只有一个页面时，它必须是根——它可以附加到 btree 元数据页面，我们就完成了。
 *
 *	将页面完全填满是不可明智的，因为那样 *任何* 插入都会导致拆分（不仅仅是叶子页面；拆分的需要会向上级传播）。通常，btree 的稳态负载因子估计为 70%。我们选择将叶页面打包到用户可控的填充因子（默认 90%），同时上级页面总是打包到 70%。这为我们提供了合理的密度（如果键的大小合理，上级页面就不多），而不会在早期插入期间引发大量级联拆分。
 *
 *	以前正在构建的索引页面存储在共享缓冲区中，但这没有任何价值（因为其他后端尚不感兴趣），并且它为 CHECKPOINT 创建了锁定问题，因为上级页面长时间保持独占锁定。现在我们只在本地内存中构建页，并在完成后使用 smgrwrite 或 smgrextend 它们。它们将需要在构建完成后第一次使用时重新读取到共享缓冲区中。
 *
 *	这段代码完全不关心 FSM。调用者负责初始化它。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *	  src/backend/access/nbtree/nbtsort.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include "access/nbtree.h"
#include "access/parallel.h"
#include "access/relscan.h"
#include "access/table.h"
#include "access/xact.h"
#include "access/xlog.h"
#include "access/xloginsert.h"
#include "catalog/index.h"
#include "commands/progress.h"
#include "executor/instrument.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "storage/smgr.h"
#include "tcop/tcopprot.h"		/* pgrminclude 忽略 */
#include "utils/rel.h"
#include "utils/sortsupport.h"
#include "utils/tuplesort.h"


/* 并行状态共享的魔法数字 */
#define PARALLEL_KEY_BTREE_SHARED		UINT64CONST(0xA000000000000001)
#define PARALLEL_KEY_TUPLESORT			UINT64CONST(0xA000000000000002)
#define PARALLEL_KEY_TUPLESORT_SPOOL2	UINT64CONST(0xA000000000000003)
#define PARALLEL_KEY_QUERY_TEXT			UINT64CONST(0xA000000000000004)
#define PARALLEL_KEY_WAL_USAGE			UINT64CONST(0xA000000000000005)
#define PARALLEL_KEY_BUFFER_USAGE		UINT64CONST(0xA000000000000006)

/*
 * DISABLE_LEADER_PARTICIPATION 禁用领导者参与
 * 并行索引构建。这可能作为调试工具很有用。
#undef DISABLE_LEADER_PARTICIPATION
 */

/*
 * 列举/排序阶段的状态记录。 （请注意，由于
 * 死元组的唯一性检查的特殊要求，我们可能有两个
 * 这样的记录。）
 */
typedef struct BTSpool
{
	Tuplesortstate *sortstate;	/* tuplesort.c的状态数据 */
	Relation	heap;
	Relation	index;
	bool		isunique;
	bool		nulls_not_distinct;
} BTSpool;

/*
 * 并行进行的索引构建的状态。 这在一个
 * 动态共享内存段中分配。 请注意，有一个单独的tuplesort TOC
 * 条目，私有于tuplesort.c，但由这个模块代表其分配。
 */
typedef struct BTShared
{
	/*
	 * 这些字段在排序过程中不会被修改。它们主要存在
	 * 是为了让需要创建与领导者所用的BTSpool状态相对应的
	 * 工作进程受益。
	 */
	Oid			heaprelid;
	Oid			indexrelid;
	bool		isunique;
	bool		nulls_not_distinct;
	bool		isconcurrent;
	int			scantuplesortstates;

	/*
	 * workersdonecv用于监视工人的进度。所有并行
	 * 参与者必须在领导者可以使用
	 * 工作人员在扫描过程中维护的可变状态之前，
	 * 声明他们已完成（并在领导者可以
	 * 继续到tuplesort_performsort()之前）。
	 */
	ConditionVariable workersdonecv;

	/*
	 * mutex保护所有字段，直至heapdesc。
	 *
	 * 这些字段包含对B树索引构建感兴趣的状态信息，
	 * 当并行构建索引时必须以相同方式工作。
	 */
	slock_t		mutex;

	/*
	 * 由工作人员维护的可变状态，并在并行扫描结束时
	 * 返回给领导者。
	 *
	 * nparticipantsdone是已完成的工作进程数量。
	 *
	 * reltuples是输入堆元组的总数。
	 *
	 * havedead指示在构建过程中是否遇到最近死亡的元组。
	 *
	 * indtuples是进入索引的元组总数。
	 *
	 * brokenhotchain指示是否有任何工作者在构建过程中
	 * 检测到损坏的HOT链。
	 */
	int			nparticipantsdone;
	double		reltuples;
	bool		havedead;
	double		indtuples;
	bool		brokenhotchain;

	/*
	 * ParallelTableScanDescData 数据紧随其后。不能直接嵌入此处，
	 * 因为并行表扫描描述接口的实现可能需要
	 * 更强的对齐。
	 */
} BTShared;

/*
 * 返回指向BTShared的并行表扫描的指针。
 *
 * 参见shm_toc_allocate，了解为何使用BUFFERALIGN，而不仅仅是
 * MAXALIGN。
 */
#define ParallelTableScanFromBTShared(shared) \
	(ParallelTableScanDesc) ((char *) (shared) + BUFFERALIGN(sizeof(BTShared)))

/*
 * 并行索引构建中的领导者状态。
 */
typedef struct BTLeader
{
	/* 并行上下文本身 */
	ParallelContext *pcxt;

	/*
	 * nparticipanttuplesorts是成功启动的工作进程的确切数量，
	 * 加上一个领导者进程，如果它作为一个
	 * 工人参与（只有DISABLE_LEADER_PARTICIPATION构建避免领导者
	 * 作为工人参与）。
	 */
	int			nparticipanttuplesorts;

	/*
	 * 领导进程方便访问共享状态（领导避免 TOC 查询）。
	 *
	 * btshared 是整个构建的共享状态。 sharedsort 是传递给每个进程 tuplesort 的共享、tuplesort 管理的状态。 sharedsort2 是对应的 btspool2 共享状态，仅在构建唯一索引时使用。 snapshot 是扫描所使用的快照，前提是需要 MVCC 快照。
	 */
	BTShared   *btshared;
	Sharedsort *sharedsort;
	Sharedsort *sharedsort2;
	Snapshot	snapshot;
	WalUsage   *walusage;
	BufferUsage *bufferusage;
} BTLeader;

/*
 * btbuild 和其回调的工作状态。
 *
 * 当使用并行创建索引时，每个参与者都有一个 BTBuildState。
 */
typedef struct BTBuildState
{
	bool		isunique;
	bool		nulls_not_distinct;
	bool		havedead;
	Relation	heap;
	BTSpool    *spool;

	/*
	 * 仅在索引为唯一索引时需要 spool2。死元组被放入 spool2 而不是 spool，以避免唯一性检查。
	 */
	BTSpool    *spool2;
	double		indtuples;

	/*
	 * btleader 仅在执行并行索引构建时存在，并且仅在领导进程中。（实际上，只有领导者有一个 BTBuildState。工作进程有自己的 spool 和 spool2。）
	 */
	BTLeader   *btleader;
} BTBuildState;

/*
 * 被构建的 btree 页面的状态记录。我们为每个活动树级都有一个这样的记录。
 */
typedef struct BTPageState
{
	Page		btps_page;		/* 页面构建的工作区 */
	BlockNumber btps_blkno;		/* 要写入该页面的块 # */
	IndexTuple	btps_lowkey;	/* 页面的严格下限枢轴元组 */
	OffsetNumber btps_lastoff;	/* 加载的最后项偏移量 */
	Size		btps_lastextra; /* 最后一项的额外发布列表空间 */
	uint32		btps_level;		/* 树级别 (0 = 叶子) */
	Size		btps_full;		/* 如果可用空间少于这个，则为“已满” */
	struct BTPageState *btps_next;	/* 指向父级别的链接（如果有） */
} BTPageState;

/*
 * 索引写入阶段的整体状态记录。
 */
typedef struct BTWriteState
{
	Relation	heap;
	Relation	index;
	BTScanInsert inskey;		/* 通用插入扫描键 */
	bool		btws_use_wal;	/* 是否将页面转储到 WAL？ */
	BlockNumber btws_pages_alloced; /* 分配的页面数量 */
	BlockNumber btws_pages_written; /* 写出的页面数量 */
	Page		btws_zeropage;	/* 填充零的工作区 */
} BTWriteState;


static double fc__bt_spools_heapscan(Relation fc_heap, Relation fc_index,
								  BTBuildState *fc_buildstate, IndexInfo *fc_indexInfo);
static void fc__bt_spooldestroy(BTSpool *fc_btspool);
static void fc__bt_spool(BTSpool *fc_btspool, ItemPointer fc_self,
					  Datum *fc_values, bool *fc_isnull);
static void fc__bt_leafbuild(BTSpool *fc_btspool, BTSpool *fc_btspool2);
static void fc__bt_build_callback(Relation fc_index, ItemPointer fc_tid, Datum *fc_values,
							   bool *fc_isnull, bool fc_tupleIsAlive, void *fc_state);
static Page fc__bt_blnewpage(uint32 fc_level);
static BTPageState *fc__bt_pagestate(BTWriteState *fc_wstate, uint32 fc_level);
static void fc__bt_slideleft(Page fc_rightmostpage);
static void fc__bt_sortaddtup(Page fc_page, Size fc_itemsize,
						   IndexTuple fc_itup, OffsetNumber fc_itup_off,
						   bool fc_newfirstdataitem);
static void fc__bt_buildadd(BTWriteState *fc_wstate, BTPageState *fc_state,
						 IndexTuple fc_itup, Size fc_truncextra);
static void fc__bt_sort_dedup_finish_pending(BTWriteState *fc_wstate,
										  BTPageState *fc_state,
										  BTDedupState fc_dstate);
static void fc__bt_uppershutdown(BTWriteState *fc_wstate, BTPageState *fc_state);
static void fc__bt_load(BTWriteState *fc_wstate,
					 BTSpool *fc_btspool, BTSpool *fc_btspool2);
static void fc__bt_begin_parallel(BTBuildState *fc_buildstate, bool fc_isconcurrent,
							   int fc_request);
static void fc__bt_end_parallel(BTLeader *fc_btleader);
static Size fc__bt_parallel_estimate_shared(Relation fc_heap, Snapshot fc_snapshot);
static double fc__bt_parallel_heapscan(BTBuildState *fc_buildstate,
									bool *fc_brokenhotchain);
static void fc__bt_leader_participate_as_worker(BTBuildState *fc_buildstate);
static void fc__bt_parallel_scan_and_sort(BTSpool *fc_btspool, BTSpool *fc_btspool2,
									   BTShared *fc_btshared, Sharedsort *fc_sharedsort,
									   Sharedsort *fc_sharedsort2, int fc_sortmem,
									   bool fc_progress);


/*
 *	btbuild() -- 构建一个新的 btree 索引。
 */
IndexBuildResult * btbuild(Relation fc_heap, Relation fc_index, IndexInfo *fc_indexInfo)
{
	IndexBuildResult *fc_result;
	BTBuildState fc_buildstate;
	double		fc_reltuples;

#ifdef BTREE_BUILD_STATS
	if (log_btree_build_stats)
		ResetUsage();
#endif							/* BTREE_BUILD_STATS */

	fc_buildstate.isunique = fc_indexInfo->ii_Unique;
	fc_buildstate.nulls_not_distinct = fc_indexInfo->ii_NullsNotDistinct;
	fc_buildstate.havedead = false;
	fc_buildstate.heap = fc_heap;
	fc_buildstate.spool = NULL;
	fc_buildstate.spool2 = NULL;
	fc_buildstate.indtuples = 0;
	fc_buildstate.btleader = NULL;

	/*
	 * 我们希望每个索引关系恰好调用一次。如果不是，
	 * 那将是大麻烦。
	 */
	if (RelationGetNumberOfBlocks(fc_index) != 0)
		elog(ERROR, "index \"%s\" already contains data",
			 RelationGetRelationName(fc_index));

	fc_reltuples = fc__bt_spools_heapscan(fc_heap, fc_index, &fc_buildstate, fc_indexInfo);

	/*
	 * 通过（1）完成 spool 文件的排序，（2）将排序后的元组插入 btree 页面，以及（3）构建上层来完成构建。最后，可能还需要结束并行使用。
	 */
	fc__bt_leafbuild(fc_buildstate.spool, fc_buildstate.spool2);
	fc__bt_spooldestroy(fc_buildstate.spool);
	if (fc_buildstate.spool2)
		fc__bt_spooldestroy(fc_buildstate.spool2);
	if (fc_buildstate.btleader)
		fc__bt_end_parallel(fc_buildstate.btleader);

	fc_result = (IndexBuildResult *) palloc(sizeof(IndexBuildResult));

	fc_result->heap_tuples = fc_reltuples;
	fc_result->index_tuples = fc_buildstate.indtuples;

#ifdef BTREE_BUILD_STATS
	if (log_btree_build_stats)
	{
		ShowUsage("BTREE BUILD STATS");
		ResetUsage();
	}
#endif							/* BTREE_BUILD_STATS */

	return fc_result;
}

/*
 * 创建并初始化一个或两个 spool 结构，并将它们保存在调用者的 buildstate 参数中。还可以填充 indexInfo 中与索引构建相关的字段。
 *
 * 扫描堆，可能并行地，用 IndexTuples 填充 spools。该例程封装了管理并行性的所有方面。调用者只需在完成 spool/spool2 后在并行情况下调用 _bt_end_parallel()。
 *
 * 返回扫描的堆元组总数。
 */
static double fc__bt_spools_heapscan(Relation fc_heap, Relation fc_index, BTBuildState *fc_buildstate,
					IndexInfo *fc_indexInfo)
{
	BTSpool    *fc_btspool = (BTSpool *) palloc0(sizeof(BTSpool));
	SortCoordinate fc_coordinate = NULL;
	double		fc_reltuples = 0;

	/*
	 * 我们将排序区域的大小设置为 maintenance_work_mem 而不是 work_mem，以加快索引创建速度。这应该是可以的，因为单个后端无法同时运行多个索引创建（另见下文关于并行性和 maintenance_work_mem 的说明）。
	 */
	fc_btspool->heap = fc_heap;
	fc_btspool->index = fc_index;
	fc_btspool->isunique = fc_indexInfo->ii_Unique;
	fc_btspool->nulls_not_distinct = fc_indexInfo->ii_NullsNotDistinct;

	/* 保存为主 spool */
	fc_buildstate->spool = fc_btspool;

	/* 报告表扫描阶段已开始 */
	pgstat_progress_update_param(PROGRESS_CREATEIDX_SUBPHASE,
								 PROGRESS_BTREE_PHASE_INDEXBUILD_TABLESCAN);

	/* 在需要时尝试启动并行工作扫描 */
	if (fc_indexInfo->ii_ParallelWorkers > 0)
		fc__bt_begin_parallel(fc_buildstate, fc_indexInfo->ii_Concurrent,
						   fc_indexInfo->ii_ParallelWorkers);

	/*
	 * 如果请求并行构建并且至少一个工作进程成功启动，
	 * 设置协调状态
	 */
	if (fc_buildstate->btleader)
	{
		fc_coordinate = (SortCoordinate) palloc0(sizeof(SortCoordinateData));
		fc_coordinate->isWorker = false;
		fc_coordinate->nParticipants =
			fc_buildstate->btleader->nparticipanttuplesorts;
		fc_coordinate->sharedsort = fc_buildstate->btleader->sharedsort;
	}

	/*
	 * 开始串行/领导者元组排序。
	 *
	 * 在涉及并行的情况下，领导者接收到与串行排序相同的
	 * maintenance_work_mem（在返回后通常按串行排序的方式处理）。 
	 * 并行工作者 Tuplesortstates 将只接收到一部分 
	 * maintenance_work_mem，然而。
	 *
	 * 我们依赖于领导者 Tuplesortstate 的生命周期几乎不与任何工作者 
	 * Tuplesortstate 的生命周期重叠。可能会有一些小的重叠， 
	 * 但这没关系，因为我们依靠领导者 Tuplesortstate 在这里只分配 
	 * 少量固定内存。当其 tuplesort_performsort() 被调用时 
	 * （通过我们的调用方），并且可能使用大量内存时，所有工作者必须 
	 * 已经释放了几乎所有由其 Tuplesortstates 持有的内存 
	 * （它们也即将完全消失）。总体效果是 
	 * maintenance_work_mem 始终代表 CREATE INDEX 操作使用的内存量的绝对高水位线，无论 
	 * 是否使用并行或任何其他因素。
	 */
	fc_buildstate->spool->sortstate =
		tuplesort_begin_index_btree(fc_heap, fc_index, fc_buildstate->isunique,
									fc_buildstate->nulls_not_distinct,
									maintenance_work_mem, fc_coordinate,
									TUPLESORT_NONE);

	/*
	 * 如果构建唯一索引，将死元组放入第二个临时存储，以使
	 * 其不参与唯一性检查。我们预计第二个临时存储（用于死元组）不会变得太满，
	 * 所以我们只给它 work_mem。
	 */
	if (fc_indexInfo->ii_Unique)
	{
		BTSpool    *fc_btspool2 = (BTSpool *) palloc0(sizeof(BTSpool));
		SortCoordinate fc_coordinate2 = NULL;

		/* 初始化次要临时存储 */
		fc_btspool2->heap = fc_heap;
		fc_btspool2->index = fc_index;
		fc_btspool2->isunique = false;
		/* 保存为次要临时存储 */
		fc_buildstate->spool2 = fc_btspool2;

		if (fc_buildstate->btleader)
		{
			/*
			 * 设置传递给
			 * tuplesort_begin_index_btree() 的非私有状态，关于
			 * 并行排序的基本高水平协调。
			 */
			fc_coordinate2 = (SortCoordinate) palloc0(sizeof(SortCoordinateData));
			fc_coordinate2->isWorker = false;
			fc_coordinate2->nParticipants =
				fc_buildstate->btleader->nparticipanttuplesorts;
			fc_coordinate2->sharedsort = fc_buildstate->btleader->sharedsort2;
		}

		/*
		 * 我们预计第二个（用于死元组）不会变得太满，
		 * 所以我们只给它 work_mem
		 */
		fc_buildstate->spool2->sortstate =
			tuplesort_begin_index_btree(fc_heap, fc_index, false, false, work_mem,
										fc_coordinate2, TUPLESORT_NONE);
	}

	/* 使用串行或并行堆扫描填充临时存储 */
	if (!fc_buildstate->btleader)
		fc_reltuples = table_index_build_scan(fc_heap, fc_index, fc_indexInfo, true, true,
										   fc__bt_build_callback, (void *) fc_buildstate,
										   NULL);
	else
		fc_reltuples = fc__bt_parallel_heapscan(fc_buildstate,
										  &fc_indexInfo->ii_BrokenHotChain);

	/*
	 * 为下一阶段设定进度目标。重置由 table_index_build_scan 设置的块编号
	 */
	{
		const int	fc_progress_index[] = {
			PROGRESS_CREATEIDX_TUPLES_TOTAL,
			PROGRESS_SCAN_BLOCKS_TOTAL,
			PROGRESS_SCAN_BLOCKS_DONE
		};
		const int64 fc_progress_vals[] = {
			fc_buildstate->indtuples,
			0, 0
		};

		pgstat_progress_update_multi_param(3, fc_progress_index, fc_progress_vals);
	}

	/* 好的，所有堆元组都已临时存储 */
	if (fc_buildstate->spool2 && !fc_buildstate->havedead)
	{
		/* spool2 证明是多余的 */
		fc__bt_spooldestroy(fc_buildstate->spool2);
		fc_buildstate->spool2 = NULL;
	}

	return fc_reltuples;
}

/*
 * 清理排队结构及其子结构。
 */
static void fc__bt_spooldestroy(BTSpool *fc_btspool)
{
	tuplesort_end(fc_btspool->sortstate);
	pfree(fc_btspool);
}

/*
 * 将索引条目放入排序文件中。
 */
static void fc__bt_spool(BTSpool *fc_btspool, ItemPointer fc_self, Datum *fc_values, bool *fc_isnull)
{
	tuplesort_putindextuplevalues(fc_btspool->sortstate, fc_btspool->index,
								  fc_self, fc_values, fc_isnull);
}

/*
 * 给定一个通过连续调用 _bt_spool 加载的临时存储，
 * 创建整个 btree。
 */
static void fc__bt_leafbuild(BTSpool *fc_btspool, BTSpool *fc_btspool2)
{
	BTWriteState fc_wstate;

#ifdef BTREE_BUILD_STATS
	if (log_btree_build_stats)
	{
		ShowUsage("BTREE BUILD (Spool) STATISTICS");
		ResetUsage();
	}
#endif							/* BTREE_BUILD_STATS */

	/* 执行排序 */
	pgstat_progress_update_param(PROGRESS_CREATEIDX_SUBPHASE,
								 PROGRESS_BTREE_PHASE_PERFORMSORT_1);
	tuplesort_performsort(fc_btspool->sortstate);
	if (fc_btspool2)
	{
		pgstat_progress_update_param(PROGRESS_CREATEIDX_SUBPHASE,
									 PROGRESS_BTREE_PHASE_PERFORMSORT_2);
		tuplesort_performsort(fc_btspool2->sortstate);
	}

	fc_wstate.heap = fc_btspool->heap;
	fc_wstate.index = fc_btspool->index;
	fc_wstate.inskey = _bt_mkscankey(fc_wstate.index, NULL);
	/* _bt_mkscankey() 不会在没有 metapage 的情况下设置 allequalimage */
	fc_wstate.inskey->allequalimage = _bt_allequalimage(fc_wstate.index, true);
	fc_wstate.btws_use_wal = RelationNeedsWAL(fc_wstate.index);

	/* 保留 metapage */
	fc_wstate.btws_pages_alloced = BTREE_METAPAGE + 1;
	fc_wstate.btws_pages_written = 0;
	fc_wstate.btws_zeropage = NULL;	/* 等待需要时再使用 */

	pgstat_progress_update_param(PROGRESS_CREATEIDX_SUBPHASE,
								 PROGRESS_BTREE_PHASE_LEAF_LOAD);
	fc__bt_load(&fc_wstate, fc_btspool, fc_btspool2);
}

/*
 * 每元组的回调，用于table_index_build_scan
 */
static void fc__bt_build_callback(Relation fc_index,
				   ItemPointer fc_tid,
				   Datum *fc_values,
				   bool *fc_isnull,
				   bool fc_tupleIsAlive,
				   void *fc_state)
{
	BTBuildState *fc_buildstate = (BTBuildState *) fc_state;

	/*
	 * 将索引元组插入到适当的临时存储文件中以便后续处理
	 */
	if (fc_tupleIsAlive || fc_buildstate->spool2 == NULL)
		fc__bt_spool(fc_buildstate->spool, fc_tid, fc_values, fc_isnull);
	else
	{
		/* 死元组放入 spool2 */
		fc_buildstate->havedead = true;
		fc__bt_spool(fc_buildstate->spool2, fc_tid, fc_values, fc_isnull);
	}

	fc_buildstate->indtuples += 1;
}

/*
 * 为一个新的、干净的 btree 页分配工作空间，未链接到任何兄弟页面。
 */
static Page fc__bt_blnewpage(uint32 fc_level)
{
	Page		fc_page;
	BTPageOpaque fc_opaque;

	fc_page = (Page) palloc(BLCKSZ);

	/* 将页面清零并设置标准页面头信息 */
	_bt_pageinit(fc_page, BLCKSZ);

	/* 初始化 BT 不透明状态 */
	fc_opaque = BTPageGetOpaque(fc_page);
	fc_opaque->btpo_prev = fc_opaque->btpo_next = P_NONE;
	fc_opaque->btpo_level = fc_level;
	fc_opaque->btpo_flags = (fc_level > 0) ? 0 : BTP_LEAF;
	fc_opaque->btpo_cycleid = 0;

	/* 使 P_HIKEY 行指针看起来被分配 */
	((PageHeader) fc_page)->pd_lower += sizeof(ItemIdData);

	return fc_page;
}

/*
 * 发出已完成的 btree 页面，并释放工作存储。
 */
static void fc__bt_blwritepage(BTWriteState *fc_wstate, Page fc_page, BlockNumber fc_blkno)
{
	/* XLOG 相关内容 */
	if (fc_wstate->btws_use_wal)
	{
		/* 我们使用 XLOG_FPI 记录类型来实现这个 */
		log_newpage(&fc_wstate->index->rd_node, MAIN_FORKNUM, fc_blkno, fc_page, true);
	}

	/*
	 * 如果我们必须非顺序地写入页面，填充空间为0，直到我们返回并覆盖。 
	 * 在标准 Unix 文件系统上，这在逻辑上并不是必要的（未写入的空间反正会读取为0）， 
	 * 但它应该有助于避免碎片化。然而，虚拟页面并没有进行 WAL 日志记录。
	 */
	while (fc_blkno > fc_wstate->btws_pages_written)
	{
		if (!fc_wstate->btws_zeropage)
			fc_wstate->btws_zeropage = (Page) palloc0(BLCKSZ);
		/* 不要为全零页面设置校验和 */
		smgrextend(RelationGetSmgr(fc_wstate->index), MAIN_FORKNUM,
				   fc_wstate->btws_pages_written++,
				   (char *) fc_wstate->btws_zeropage,
				   true);
	}

	PageSetChecksumInplace(fc_page, fc_blkno);

	/*
	 * 现在写入页面。smgr 不需要为这一写入调度 fsync；我们会在结束构建之前自己执行。
	 */
	if (fc_blkno == fc_wstate->btws_pages_written)
	{
		/* 正在扩展文件... */
		smgrextend(RelationGetSmgr(fc_wstate->index), MAIN_FORKNUM, fc_blkno,
				   (char *) fc_page, true);
		fc_wstate->btws_pages_written++;
	}
	else
	{
		/* 正在覆盖之前填充为零的区块 */
		smgrwrite(RelationGetSmgr(fc_wstate->index), MAIN_FORKNUM, fc_blkno,
				  (char *) fc_page, true);
	}

	pfree(fc_page);
}

/*
 * 分配并初始化一个新的 BTPageState。返回的结构 
 * 适合被 _bt_buildadd 直接使用。
 */
static BTPageState *
fc__bt_pagestate(BTWriteState *fc_wstate, uint32 fc_level)
{
	BTPageState *fc_state = (BTPageState *) palloc0(sizeof(BTPageState));

	/* 为层创建初始页面 */
	fc_state->btps_page = fc__bt_blnewpage(fc_level);

	/* 并赋予其页面位置 */
	fc_state->btps_blkno = fc_wstate->btws_pages_alloced++;

	fc_state->btps_lowkey = NULL;
	/* 初始化 lastoff 以便第一个项目进入 P_FIRSTKEY */
	fc_state->btps_lastoff = P_HIKEY;
	fc_state->btps_lastextra = 0;
	fc_state->btps_level = fc_level;
	/* 根据级别设置“满”阈值。请参阅文件开头的注释。 */
	if (fc_level > 0)
		fc_state->btps_full = (BLCKSZ * (100 - BTREE_NONLEAF_FILLFACTOR) / 100);
	else
		fc_state->btps_full = BTGetTargetPageFreeSpace(fc_wstate->index);

	/* 还没有父级别 */
	fc_state->btps_next = NULL;

	return fc_state;
}

/*
 * 将页面的 ItemIds 数组向后滑动一个位置（从 P_FIRSTKEY 到
 * P_HIKEY，覆盖 P_HIKEY）。
 *
 * _bt_blnewpage() 使 P_HIKEY 行指针看起来已被分配，但
 * 它的级别最右侧的页面不应该获得高键。现在清楚这个页面是最右侧的页面，
 * 移除不需要的空 P_HIKEY 行指针空间。
 */
static void fc__bt_slideleft(Page fc_rightmostpage)
{
	OffsetNumber fc_off;
	OffsetNumber fc_maxoff;
	ItemId		fc_previi;

	fc_maxoff = PageGetMaxOffsetNumber(fc_rightmostpage);
	Assert(fc_maxoff >= P_FIRSTKEY);
	fc_previi = PageGetItemId(fc_rightmostpage, P_HIKEY);
	for (fc_off = P_FIRSTKEY; fc_off <= fc_maxoff; fc_off = OffsetNumberNext(fc_off))
	{
		ItemId		fc_thisii = PageGetItemId(fc_rightmostpage, fc_off);

		*fc_previi = *fc_thisii;
		fc_previi = fc_thisii;
	}
	((PageHeader) fc_rightmostpage)->pd_lower -= sizeof(ItemIdData);
}

/*
 * 向正在构建的页面添加一个项目。
 *
 * 这与 nbtinsert.c 的 _bt_pgaddtup() 非常相似，但这个变体 
 * 直接抛出错误。
 *
 * 请注意，我们的 nbtsort.c 调用者尚不知道页面是否会最右。 
 * 偏移 P_FIRSTKEY 总是被调用者假设为第一个数据键。 
 * 最终在其级别上被确定为最右的页面是通过调用 _bt_slideleft() 修复的。
 */
static void fc__bt_sortaddtup(Page fc_page,
			   Size fc_itemsize,
			   IndexTuple fc_itup,
			   OffsetNumber fc_itup_off,
			   bool fc_newfirstdataitem)
{
	IndexTupleData fc_trunctuple;

	if (fc_newfirstdataitem)
	{
		fc_trunctuple = *fc_itup;
		fc_trunctuple.t_info = sizeof(IndexTupleData);
		BTreeTupleSetNAtts(&fc_trunctuple, 0, false);
		fc_itup = &fc_trunctuple;
		fc_itemsize = sizeof(IndexTupleData);
	}

	if (PageAddItem(fc_page, (Item) fc_itup, fc_itemsize, fc_itup_off,
					false, false) == InvalidOffsetNumber)
		elog(ERROR, "failed to add item to the index page");
}

/*----------
 * 从排序输出中向磁盘页面添加项目（或添加由排序输出形成的发布列表项目）。
 *
 * 我们必须小心遵循 nbtsearch.c 的页面布局约定：
 * - 最右边的页面从 P_HIKEY 开始数据项，而不是从 P_FIRSTKEY 开始。
 * - 在非叶页面上，第一个项目的键部分不必存储，我们只需存储链接。
 *
 * 构建中的叶页面如下所示：
 *
 * +----------------+---------------------------------+
 * | PageHeaderData | linp0 linp1 linp2 ...           |
 * +-----------+----+---------------------------------+
 * | ... linpN |									  |
 * +-----------+--------------------------------------+
 * |	 ^ last										  |
 * |												  |
 * +-------------+------------------------------------+
 * |			 | itemN ...                          |
 * +-------------+------------------+-----------------+
 * |		  ... item3 item2 item1 | "special space" |
 * +--------------------------------+-----------------+
 *
 * 与 bufpage.h 中的图示进行对比；注意 linps 和 items 之间的不匹配。这是因为我们保留 linp0 作为
 * “高键”项目的指针的占位符；当我们填满页面后，将 linp0 设置为指向 itemN 并清除
 * linpN。另一方面，如果我们发现这是最后一（最右边的）页面，我们会保持项目不变并将
 * linp 数组滑动。如果高键要被截断，偏移 1 被删除，我们在偏移 1 插入截断的高键。
 *
 * 'last' 指针指示添加到页面的最后偏移量。
 *
 * 'truncextra' 是 itup 中发布列表的大小（如果有的话）。这个信息被存储在这里的下一次调用中，
 * 当我们可能考虑截断发布列表对页面的影响时，以便在决定完成页面之前进行参考。
 * 发布列表通常相对较大，因此值得花精力考虑从截断成为高键的元组的发布列表中节省的空间
 * （这可能是接近页面目标自由空间的唯一方法）。请注意，这仅用于软填充因子范围的限制，而不是
 * 严格的硬限制。
 *----------
 */
static void fc__bt_buildadd(BTWriteState *fc_wstate, BTPageState *fc_state, IndexTuple fc_itup,
			 Size fc_truncextra)
{
	Page		fc_npage;
	BlockNumber fc_nblkno;
	OffsetNumber fc_last_off;
	Size		fc_last_truncextra;
	Size		fc_pgspc;
	Size		fc_itupsz;
	bool		fc_isleaf;

	/*
	 * 这是一个方便的地方，可以在索引创建的 B 树加载阶段检查取消中断。
	 */
	CHECK_FOR_INTERRUPTS();

	fc_npage = fc_state->btps_page;
	fc_nblkno = fc_state->btps_blkno;
	fc_last_off = fc_state->btps_lastoff;
	fc_last_truncextra = fc_state->btps_lastextra;
	fc_state->btps_lastextra = fc_truncextra;

	fc_pgspc = PageGetFreeSpace(fc_npage);
	fc_itupsz = IndexTupleSize(fc_itup);
	fc_itupsz = MAXALIGN(fc_itupsz);
	/* 叶子情况由于后缀截断而有一些不同的规则 */
	fc_isleaf = (fc_state->btps_level == 0);

	/*
	 * 检查新项是否可以在当前级别的btree页面上放置。
	 *
	 * 每个新构建的索引会将堆TID视为键空间的一部分，这就要求
	 * 新的高键必须偶尔在_bt_truncate()中附加一个堆TID。
	 * 这可能会让新的支点元组比其派生的原始firstright元组
	 * 大一到两个MAXALIGN()量。v4通过相同小幅度减少
	 * 插入到叶子层级的元组大小限制来解决该问题。在叶子层级
	 * 强制新的v4+限制，而在内部层级保持老限制，因为
	 * 支点元组可能需要利用预留空间。这在内部页面上
	 * 应该不会失败。
	 */
	if (unlikely(fc_itupsz > BTMaxItemSize(fc_npage)))
		_bt_check_third_page(fc_wstate->index, fc_wstate->heap, fc_isleaf, fc_npage,
							 fc_itup);

	/*
	 * 检查当前页面是否适合新项，并在页面为叶子页面时
	 * 保留空间以在后缀截断期间附加一个堆TID。
	 *
	 * 当完成一个叶子页面时，我们保证至少可以放下两个非支点
	 * 元组加上一个带堆TID的高键，因为我们依赖于
	 * _bt_check_third_page()拒绝超大的非支点元组。
	 * 在内部页面上，我们始终可以放下三个支点元组，
	 * 其较大的内部页面元组限制（包括页面高键）。
	 *
	 * 大部分时间，页面只是“满”的意思是软填充因子限制
	 * 被超过。然而，在开始新页面之前，我们必须始终在每个
	 * 页面上留下至少两个项加一个高键。如果我们还没有达到
	 * 最小的项数，则忽略填充因子并在“满”的当前页面上插入。
	 * （请注意，我们故意假设后缀截断在应用软限制时
	 * 不会增大或缩小新的高键，除非最后一个元组有一个发布列表。）
	 */
	Assert(fc_last_truncextra == 0 || fc_isleaf);
	if (fc_pgspc < fc_itupsz + (fc_isleaf ? MAXALIGN(sizeof(ItemPointerData)) : 0) ||
		(fc_pgspc + fc_last_truncextra < fc_state->btps_full && fc_last_off > P_FIRSTKEY))
	{
		/*
		 * 完成页面并将其写出。
		 */
		Page		fc_opage = fc_npage;
		BlockNumber fc_oblkno = fc_nblkno;
		ItemId		fc_ii;
		ItemId		fc_hii;
		IndexTuple	fc_oitup;

		/* 创建相同级别的新页面 */
		fc_npage = fc__bt_blnewpage(fc_state->btps_level);

		/* 并赋予其页面位置 */
		fc_nblkno = fc_wstate->btws_pages_alloced++;

		/*
		 * 我们将页面上的最后一个项复制到新页面中，
		 * 然后重新排列旧页面，使“最后一个项”成为其高键
		 * 而不是一个真正的数据项。页面上最好已经有
		 * 至少两个项，否则页面将没有有用数据。
		 */
		Assert(fc_last_off > P_FIRSTKEY);
		fc_ii = PageGetItemId(fc_opage, fc_last_off);
		fc_oitup = (IndexTuple) PageGetItem(fc_opage, fc_ii);
		fc__bt_sortaddtup(fc_npage, ItemIdGetLength(fc_ii), fc_oitup, P_FIRSTKEY,
					   !fc_isleaf);

		/*
		 * 将“最后一个”移动到opage的高键位置。
		 * _bt_blnewpage()在opage首次创建时分配了
		 * 一块空空间用于行指针，因此这只是重新排列
		 * 页面上已经分配的空间，并初始化高键行指针。
		 * （实际上，叶子页面还必须用已截断版本的
		 * oitup交换oitup，有时这个截断版本比oitup大，
		 * 但从不超过附加一个堆TID所需的空间。）
		 */
		fc_hii = PageGetItemId(fc_opage, P_HIKEY);
		*fc_hii = *fc_ii;
		ItemIdSetUnused(fc_ii);	/* 冗余 */
		((PageHeader) fc_opage)->pd_lower -= sizeof(ItemIdData);

		if (fc_isleaf)
		{
			IndexTuple	fc_lastleft;
			IndexTuple	fc_truncated;

			/*
			 * 从叶子层级的高键中截断任何不需要的属性。
			 * 这只在叶子层级进行，因为内部页面中的下行链接
			 * 要么是负无穷项，要么是从下一级复制内容。另见：
			 * _bt_split()。
			 *
			 * 我们不试图使我们的分裂点选择偏向于增加
			 * _bt_truncate()截断更多属性的可能性，
			 * 而在_bt_split()中使用的分裂点选择则要精细得多。
			 * 即便如此，这里传递给_bt_truncate()的lastleft和
			 * firstright元组在去重使用时至少不完全相等，
			 * 除非存在大量重复项（此外，唯一索引构建通常
			 * 很少或没有spool2重复项）。当分裂点位于两个
			 * 不相等的元组之间时，_bt_truncate()将避免
			 * 在新的高键中包含堆TID，这是后缀截断的
			 * 最重要的好处。
			 *
			 * 用新的截断高键直接覆盖旧项。
			 * oitup已经位于元组空间的物理起始位置，
			 * 所以这应该直接重用现有的元组空间。
			 */
			fc_ii = PageGetItemId(fc_opage, OffsetNumberPrev(fc_last_off));
			fc_lastleft = (IndexTuple) PageGetItem(fc_opage, fc_ii);

			Assert(IndexTupleSize(fc_oitup) > fc_last_truncextra);
			fc_truncated = _bt_truncate(fc_wstate->index, fc_lastleft, fc_oitup,
									 fc_wstate->inskey);
			if (!PageIndexTupleOverwrite(fc_opage, P_HIKEY, (Item) fc_truncated,
										 IndexTupleSize(fc_truncated)))
				elog(ERROR, "failed to add high key to the index page");
			pfree(fc_truncated);

			/* oitup应继续指向页面的高键 */
			fc_hii = PageGetItemId(fc_opage, P_HIKEY);
			fc_oitup = (IndexTuple) PageGetItem(fc_opage, fc_hii);
		}

		/*
		 * 使用其低键将旧页面链接到其父级。
		 * 如果我们没有父级，则必须创建一个；
		 * 这会增加一个新的btree层级。
		 */
		if (fc_state->btps_next == NULL)
			fc_state->btps_next = fc__bt_pagestate(fc_wstate, fc_state->btps_level + 1);

		Assert((BTreeTupleGetNAtts(fc_state->btps_lowkey, fc_wstate->index) <=
				IndexRelationGetNumberOfKeyAttributes(fc_wstate->index) &&
				BTreeTupleGetNAtts(fc_state->btps_lowkey, fc_wstate->index) > 0) ||
			   P_LEFTMOST(BTPageGetOpaque(fc_opage)));
		Assert(BTreeTupleGetNAtts(fc_state->btps_lowkey, fc_wstate->index) == 0 ||
			   !P_LEFTMOST(BTPageGetOpaque(fc_opage)));
		BTreeTupleSetDownLink(fc_state->btps_lowkey, fc_oblkno);
		fc__bt_buildadd(fc_wstate, fc_state->btps_next, fc_state->btps_lowkey, 0);
		pfree(fc_state->btps_lowkey);

		/*
		 * 保存旧页面的高键的副本。
		 * 它也是新页面的低键。
		 */
		fc_state->btps_lowkey = CopyIndexTuple(fc_oitup);

		/*
		 * 设置两个页面的兄弟链接。
		 */
		{
			BTPageOpaque fc_oopaque = BTPageGetOpaque(fc_opage);
			BTPageOpaque fc_nopaque = BTPageGetOpaque(fc_npage);

			fc_oopaque->btpo_next = fc_nblkno;
			fc_nopaque->btpo_prev = fc_oblkno;
			fc_nopaque->btpo_next = P_NONE;	/* 冗余 */
		}

		/*
		 * 写出旧页面。我们不再需要触碰它，所以也可以
		 * 释放opage工作区。
		 */
		fc__bt_blwritepage(fc_wstate, fc_opage, fc_oblkno);

		/*
		 * 重置last_off，指向新页面
		 */
		fc_last_off = P_FIRSTKEY;
	}

	/*
	 * 到这里，原始页面仍然是当前页面，或者创建了
	 * 新页面，成为当前页面。无论哪种情况，
	 * 当前页面肯定有空间容纳新项。
	 *
	 * 如果新项是其页面的第一个项，
	 * 它也必须是其整个层级的第一个项。
	 * 在后续同层页面中，页面的低键将从
	 * 之前页面中复制。这里生成一个负无穷低键。
	 */
	if (fc_last_off == P_HIKEY)
	{
		Assert(fc_state->btps_lowkey == NULL);
		fc_state->btps_lowkey = palloc0(sizeof(IndexTupleData));
		fc_state->btps_lowkey->t_info = sizeof(IndexTupleData);
		BTreeTupleSetNAtts(fc_state->btps_lowkey, 0, false);
	}

	/*
	 * 将新项添加到当前页面。
	 */
	fc_last_off = OffsetNumberNext(fc_last_off);
	fc__bt_sortaddtup(fc_npage, fc_itupsz, fc_itup, fc_last_off,
				   !fc_isleaf && fc_last_off == P_FIRSTKEY);

	fc_state->btps_page = fc_npage;
	fc_state->btps_blkno = fc_nblkno;
	fc_state->btps_lastoff = fc_last_off;
}

/*
 * 完成待处理的发布列表元组，并将其添加到索引中。
 * 最终元组基于保存的基础元组和保存的堆TID
 * 列表。
 *
 * 这几乎像是_bt_dedup_finish_pending()，但它使用
 * _bt_buildadd()添加一个新元组。
 */
static void fc__bt_sort_dedup_finish_pending(BTWriteState *fc_wstate, BTPageState *fc_state,
							  BTDedupState fc_dstate)
{
	Assert(fc_dstate->nitems > 0);

	if (fc_dstate->nitems == 1)
		fc__bt_buildadd(fc_wstate, fc_state, fc_dstate->base, 0);
	else
	{
		IndexTuple	fc_postingtuple;
		Size		fc_truncextra;

		/* 形成一个带发布列表的元组 */
		fc_postingtuple = _bt_form_posting(fc_dstate->base,
										fc_dstate->htids,
										fc_dstate->nhtids);
		/* 计算发布列表开销 */
		fc_truncextra = IndexTupleSize(fc_postingtuple) -
			BTreeTupleGetPostingOffset(fc_postingtuple);

		fc__bt_buildadd(fc_wstate, fc_state, fc_postingtuple, fc_truncextra);
		pfree(fc_postingtuple);
	}

	fc_dstate->nmaxitems = 0;
	fc_dstate->nhtids = 0;
	fc_dstate->nitems = 0;
	fc_dstate->phystupsize = 0;
}

/*
 * 完成写出已完成的btree。
 */
static void fc__bt_uppershutdown(BTWriteState *fc_wstate, BTPageState *fc_state)
{
	BTPageState *fc_s;
	BlockNumber fc_rootblkno = P_NONE;
	uint32		fc_rootlevel = 0;
	Page		fc_metapage;

	/*
	 * 该循环的每次迭代完成树的一个层级。
	 */
	for (fc_s = fc_state; fc_s != NULL; fc_s = fc_s->btps_next)
	{
		BlockNumber fc_blkno;
		BTPageOpaque fc_opaque;

		fc_blkno = fc_s->btps_blkno;
		fc_opaque = BTPageGetOpaque(fc_s->btps_page);

		/*
		 * 我们必须将这一层的最后一页链接到某个地方。
		 *
		 * 如果我们在顶部，那就是根节点，所以将其附加到 metapage。
		 * 否则，使用其低键为其父级添加一个条目。
		 * 这可能会导致父级的最后一页被分裂，但
		 * 那不是问题——我们还没有处理到它。
		 */
		if (fc_s->btps_next == NULL)
		{
			fc_opaque->btpo_flags |= BTP_ROOT;
			fc_rootblkno = fc_blkno;
			fc_rootlevel = fc_s->btps_level;
		}
		else
		{
			Assert((BTreeTupleGetNAtts(fc_s->btps_lowkey, fc_wstate->index) <=
					IndexRelationGetNumberOfKeyAttributes(fc_wstate->index) &&
					BTreeTupleGetNAtts(fc_s->btps_lowkey, fc_wstate->index) > 0) ||
				   P_LEFTMOST(fc_opaque));
			Assert(BTreeTupleGetNAtts(fc_s->btps_lowkey, fc_wstate->index) == 0 ||
				   !P_LEFTMOST(fc_opaque));
			BTreeTupleSetDownLink(fc_s->btps_lowkey, fc_blkno);
			fc__bt_buildadd(fc_wstate, fc_s->btps_next, fc_s->btps_lowkey, 0);
			pfree(fc_s->btps_lowkey);
			fc_s->btps_lowkey = NULL;
		}

		/*
		 * 这是最右边的页面，因此 ItemId 数组需要向后滑动一个槽位。
		 * 然后我们可以转储页面。
		 */
		fc__bt_slideleft(fc_s->btps_page);
		fc__bt_blwritepage(fc_wstate, fc_s->btps_page, fc_s->btps_blkno);
		fc_s->btps_page = NULL;	/* writepage 释放了工作空间 */
	}

	/*
	 * 作为过程的最后一步，构造 metapage 并使其
	 * 指向新的根（除非我们根本没有数据，在这种情况下
	 * 它被设置为指向 "P_NONE"）。这通过在
	 * metapage 中填充一个有效的魔术数字，将索引更改为
	 * "有效" 状态。
	 */
	fc_metapage = (Page) palloc(BLCKSZ);
	_bt_initmetapage(fc_metapage, fc_rootblkno, fc_rootlevel,
					 fc_wstate->inskey->allequalimage);
	fc__bt_blwritepage(fc_wstate, fc_metapage, BTREE_METAPAGE);
}

/*
 * 从 tuplesort 中以正确的排序顺序读取元组，并将其加载到
 * btree 叶子中。
 */
static void fc__bt_load(BTWriteState *fc_wstate, BTSpool *fc_btspool, BTSpool *fc_btspool2)
{
	BTPageState *fc_state = NULL;
	bool		fc_merge = (fc_btspool2 != NULL);
	IndexTuple	fc_itup,
				fc_itup2 = NULL;
	bool		fc_load1;
	TupleDesc	fc_tupdes = RelationGetDescr(fc_wstate->index);
	int			fc_i,
				fc_keysz = IndexRelationGetNumberOfKeyAttributes(fc_wstate->index);
	SortSupport fc_sortKeys;
	int64		fc_tuples_done = 0;
	bool		fc_deduplicate;

	fc_deduplicate = fc_wstate->inskey->allequalimage && !fc_btspool->isunique &&
		BTGetDeduplicateItems(fc_wstate->index);

	if (fc_merge)
	{
		/*
		 * 另一个用于死元组的 BTSpool 存在。现在我们必须合并
		 * btspool 和 btspool2。
		 */

		/* 合并的准备 */
		fc_itup = tuplesort_getindextuple(fc_btspool->sortstate, true);
		fc_itup2 = tuplesort_getindextuple(fc_btspool2->sortstate, true);

		/* 为每一列准备 SortSupport 数据 */
		fc_sortKeys = (SortSupport) palloc0(fc_keysz * sizeof(SortSupportData));

		for (fc_i = 0; fc_i < fc_keysz; fc_i++)
		{
			SortSupport fc_sortKey = fc_sortKeys + fc_i;
			ScanKey		fc_scanKey = fc_wstate->inskey->scankeys + fc_i;
			int16		fc_strategy;

			fc_sortKey->ssup_cxt = CurrentMemoryContext;
			fc_sortKey->ssup_collation = fc_scanKey->sk_collation;
			fc_sortKey->ssup_nulls_first =
				(fc_scanKey->sk_flags & SK_BT_NULLS_FIRST) != 0;
			fc_sortKey->ssup_attno = fc_scanKey->sk_attno;
			/* 这里不支持缩写 */
			fc_sortKey->abbreviate = false;

			AssertState(fc_sortKey->ssup_attno != 0);

			fc_strategy = (fc_scanKey->sk_flags & SK_BT_DESC) != 0 ?
				BTGreaterStrategyNumber : BTLessStrategyNumber;

			PrepareSortSupportFromIndexRel(fc_wstate->index, fc_strategy, fc_sortKey);
		}

		for (;;)
		{
			fc_load1 = true;		/* 加载下一个 BTSpool？ */
			if (fc_itup2 == NULL)
			{
				if (fc_itup == NULL)
					break;
			}
			else if (fc_itup != NULL)
			{
				int32		fc_compare = 0;

				for (fc_i = 1; fc_i <= fc_keysz; fc_i++)
				{
					SortSupport fc_entry;
					Datum		fc_attrDatum1,
								fc_attrDatum2;
					bool		fc_isNull1,
								fc_isNull2;

					fc_entry = fc_sortKeys + fc_i - 1;
					fc_attrDatum1 = index_getattr(fc_itup, fc_i, fc_tupdes, &fc_isNull1);
					fc_attrDatum2 = index_getattr(fc_itup2, fc_i, fc_tupdes, &fc_isNull2);

					fc_compare = ApplySortComparator(fc_attrDatum1, fc_isNull1,
												  fc_attrDatum2, fc_isNull2,
												  fc_entry);
					if (fc_compare > 0)
					{
						fc_load1 = false;
						break;
					}
					else if (fc_compare < 0)
						break;
				}

				/*
				 * 如果键值相等，我们根据 ItemPointer 进行排序。
				 * 这是 btree 索引所要求的，因为堆 TID 被视为
				 * 隐式的最后一个键属性，以确保索引中的所有
				 * 键在物理上是唯一的。
				 */
				if (fc_compare == 0)
				{
					fc_compare = ItemPointerCompare(&fc_itup->t_tid, &fc_itup2->t_tid);
					Assert(fc_compare != 0);
					if (fc_compare > 0)
						fc_load1 = false;
				}
			}
			else
				fc_load1 = false;

			/* 当我们看到第一个元组时，创建第一个索引页 */
			if (fc_state == NULL)
				fc_state = fc__bt_pagestate(fc_wstate, 0);

			if (fc_load1)
			{
				fc__bt_buildadd(fc_wstate, fc_state, fc_itup, 0);
				fc_itup = tuplesort_getindextuple(fc_btspool->sortstate, true);
			}
			else
			{
				fc__bt_buildadd(fc_wstate, fc_state, fc_itup2, 0);
				fc_itup2 = tuplesort_getindextuple(fc_btspool2->sortstate, true);
			}

			/* 报告进度 */
			pgstat_progress_update_param(PROGRESS_CREATEIDX_TUPLES_DONE,
										 ++fc_tuples_done);
		}
		pfree(fc_sortKeys);
	}
	else if (fc_deduplicate)
	{
		/* 合并是不必要的，去重到发布列表中 */
		BTDedupState fc_dstate;

		fc_dstate = (BTDedupState) palloc(sizeof(BTDedupStateData));
		fc_dstate->deduplicate = true; /* 未使用 */
		fc_dstate->nmaxitems = 0;	/* 未使用 */
		fc_dstate->maxpostingsize = 0; /* 稍后设置 */
		/* 关于当前待处理发布列表的基础元组的元数据 */
		fc_dstate->base = NULL;
		fc_dstate->baseoff = InvalidOffsetNumber;	/* 未使用 */
		fc_dstate->basetupsize = 0;
		/* 关于当前待处理发布列表 TID 的元数据 */
		fc_dstate->htids = NULL;
		fc_dstate->nhtids = 0;
		fc_dstate->nitems = 0;
		fc_dstate->phystupsize = 0;	/* 未使用 */
		fc_dstate->nintervals = 0; /* 未使用 */

		while ((fc_itup = tuplesort_getindextuple(fc_btspool->sortstate,
											   true)) != NULL)
		{
			/* 当我们看到第一个元组时，创建第一个索引页 */
			if (fc_state == NULL)
			{
				fc_state = fc__bt_pagestate(fc_wstate, 0);

				/*
				 * 将发布列表元组的大小限制为我们希望留在
				 * 页面上的 1/10 空间，加上最终项目的行
				 * 指针的空间。这等于我们希望留在每个叶子页面上的
				 * 空间，当 fillfactor 为 90 时，允许我们在
				 * 发生许多重复时接近 fillfactor% 的空间利用率。
				 * （这使得在构建具有许多重复项的索引时，
				 * 较高的叶子填充因子设置变得无效，但将叶子页面
				 * 填充满少量非常大的元组似乎不是一个有用的目标。）
				 */
				fc_dstate->maxpostingsize = MAXALIGN_DOWN((BLCKSZ * 10 / 100)) -
					sizeof(ItemIdData);
				Assert(fc_dstate->maxpostingsize <= BTMaxItemSize(fc_state->btps_page) &&
					   fc_dstate->maxpostingsize <= INDEX_SIZE_MASK);
				fc_dstate->htids = palloc(fc_dstate->maxpostingsize);

				/* 使用 itup 复制开始新的待处理发布列表 */
				_bt_dedup_start_pending(fc_dstate, CopyIndexTuple(fc_itup),
										InvalidOffsetNumber);
			}
			else if (_bt_keep_natts_fast(fc_wstate->index, fc_dstate->base,
										 fc_itup) > fc_keysz &&
					 _bt_dedup_save_htid(fc_dstate, fc_itup))
			{
				/*
				 * 元组等于待处理发布列表的基础元组。它的堆
				 * TID 已在状态中保存。
				 */
			}
			else
			{
				/*
				 * 元组与待处理发布列表元组不相等，或
				 * _bt_dedup_save_htid() 选择不将当前项目
				 * 合并到待处理发布列表中。
				 */
				fc__bt_sort_dedup_finish_pending(fc_wstate, fc_state, fc_dstate);
				pfree(fc_dstate->base);

				/* 使用 itup 复制开始新的待处理发布列表 */
				_bt_dedup_start_pending(fc_dstate, CopyIndexTuple(fc_itup),
										InvalidOffsetNumber);
			}

			/* 报告进度 */
			pgstat_progress_update_param(PROGRESS_CREATEIDX_TUPLES_DONE,
										 ++fc_tuples_done);
		}

		if (fc_state)
		{
			/*
			 * 处理最后一个项目（当tuplesort返回一个或多个元组时，必须有最后一个项目）
			 */
			fc__bt_sort_dedup_finish_pending(fc_wstate, fc_state, fc_dstate);
			pfree(fc_dstate->base);
			pfree(fc_dstate->htids);
		}

		pfree(fc_dstate);
	}
	else
	{
		/* 合并和去重都是不必要的 */
		while ((fc_itup = tuplesort_getindextuple(fc_btspool->sortstate,
											   true)) != NULL)
		{
			/* 当我们看到第一个元组时，创建第一个索引页 */
			if (fc_state == NULL)
				fc_state = fc__bt_pagestate(fc_wstate, 0);

			fc__bt_buildadd(fc_wstate, fc_state, fc_itup, 0);

			/* 报告进度 */
			pgstat_progress_update_param(PROGRESS_CREATEIDX_TUPLES_DONE,
										 ++fc_tuples_done);
		}
	}

	/* 关闭最终页面并写入元数据页面 */
	fc__bt_uppershutdown(fc_wstate, fc_state);

	/*
	 * 当我们WAL-记录索引页面时，我们仍然必须fsync索引文件。
	 * 由于我们是在共享缓冲区之外构建的，在构建过程中发生的CHECKPOINT
	 * 没有办法将先前写入的数据刷新到磁盘（实际上它甚至不知道该索引存在）。
	 * 后来的崩溃将从检查点重新播放WAL，因此不会重放我们
	 * 之前的WAL条目。如果我们不在这里fsync这些页面，
	 * 则当崩溃发生时它们可能仍然不在磁盘上。
	 */
	if (fc_wstate->btws_use_wal)
		smgrimmedsync(RelationGetSmgr(fc_wstate->index), MAIN_FORKNUM);
}

/*
 * 创建并行上下文，并为领导者启动工作进程。
 *
 * buildstate参数应该被初始化（除了在spools中的tuplesort状态，
 * 该状态可能会根据此处最初设置的共享状态稍后创建）。
 *
 * isconcurrent指示操作是否为CREATE INDEX CONCURRENTLY。
 *
 * request是启动的目标并行工作进程数量。
 *
 * 设置buildstate的BTLeader，调用者必须在其索引
 * 构建的最后通过将其传递给_bt_end_parallel()来关闭并行
 * 模式。如果甚至无法启动一个工作进程，则此值
 * 永远不会设置，调用者应继续进行串行索引构建。
 */
static void fc__bt_begin_parallel(BTBuildState *fc_buildstate, bool fc_isconcurrent, int fc_request)
{
	ParallelContext *fc_pcxt;
	int			fc_scantuplesortstates;
	Snapshot	fc_snapshot;
	Size		fc_estbtshared;
	Size		fc_estsort;
	BTShared   *fc_btshared;
	Sharedsort *fc_sharedsort;
	Sharedsort *fc_sharedsort2;
	BTSpool    *fc_btspool = fc_buildstate->spool;
	BTLeader   *fc_btleader = (BTLeader *) palloc0(sizeof(BTLeader));
	WalUsage   *fc_walusage;
	BufferUsage *fc_bufferusage;
	bool		fc_leaderparticipates = true;
	int			fc_querylen;

#ifdef DISABLE_LEADER_PARTICIPATION
	fc_leaderparticipates = false;
#endif

	/*
	 * 进入并行模式，并为btree索引的并行构建创建上下文
	 */
	EnterParallelMode();
	Assert(fc_request > 0);
	fc_pcxt = CreateParallelContext("postgres", "_bt_parallel_build_main",
								 fc_request);

	fc_scantuplesortstates = fc_leaderparticipates ? fc_request + 1 : fc_request;

	/*
	 * 为基本关系的扫描做准备。在正常的索引构建中，我们使用
	 * SnapshotAny，因为我们必须检索所有元组并进行自己的时间
	 * 资格检查（因为我们必须索引最近死去的元组）。在
	 * 并发构建中，我们采用常规的MVCC快照，并根据该快照索引
	 * 任何活动的内容。
	 */
	if (!fc_isconcurrent)
		fc_snapshot = SnapshotAny;
	else
		fc_snapshot = RegisterSnapshot(GetTransactionSnapshot());

	/*
	 * 估算我们自己的PARALLEL_KEY_BTREE_SHARED工作区的大小，以及
	 * PARALLEL_KEY_TUPLESORT tuplesort工作区的大小
	 */
	fc_estbtshared = fc__bt_parallel_estimate_shared(fc_btspool->heap, fc_snapshot);
	shm_toc_estimate_chunk(&fc_pcxt->estimator, fc_estbtshared);
	fc_estsort = tuplesort_estimate_shared(fc_scantuplesortstates);
	shm_toc_estimate_chunk(&fc_pcxt->estimator, fc_estsort);

	/*
	 * 独特情况需要第二个spool，因此我们可能需要为此考虑
	 * 另一个共享工作区 -- PARALLEL_KEY_TUPLESORT_SPOOL2
	 */
	if (!fc_btspool->isunique)
		shm_toc_estimate_keys(&fc_pcxt->estimator, 2);
	else
	{
		shm_toc_estimate_chunk(&fc_pcxt->estimator, fc_estsort);
		shm_toc_estimate_keys(&fc_pcxt->estimator, 3);
	}

	/*
	 * 估算WalUsage和BufferUsage的空间 -- PARALLEL_KEY_WAL_USAGE
	 * 和PARALLEL_KEY_BUFFER_USAGE。
	 *
	 * 如果没有关心的扩展加载，我们可以跳过这一点。我们
	 * 无法知道是否有人在查看pgWalUsage或
	 * pgBufferUsage，因此无条件执行。
	 */
	shm_toc_estimate_chunk(&fc_pcxt->estimator,
						   mul_size(sizeof(WalUsage), fc_pcxt->nworkers));
	shm_toc_estimate_keys(&fc_pcxt->estimator, 1);
	shm_toc_estimate_chunk(&fc_pcxt->estimator,
						   mul_size(sizeof(BufferUsage), fc_pcxt->nworkers));
	shm_toc_estimate_keys(&fc_pcxt->estimator, 1);

	/* 最后，估算PARALLEL_KEY_QUERY_TEXT的空间 */
	if (debug_query_string)
	{
		fc_querylen = strlen(debug_query_string);
		shm_toc_estimate_chunk(&fc_pcxt->estimator, fc_querylen + 1);
		shm_toc_estimate_keys(&fc_pcxt->estimator, 1);
	}
	else
		fc_querylen = 0;			/* 保持编译器安静 */

	/* 每个人都已经有机会请求空间，因此现在创建DSM */
	InitializeParallelDSM(fc_pcxt);

	/* 如果没有可用的DSM段，则退回（进行串行构建） */
	if (fc_pcxt->seg == NULL)
	{
		if (IsMVCCSnapshot(fc_snapshot))
			UnregisterSnapshot(fc_snapshot);
		DestroyParallelContext(fc_pcxt);
		ExitParallelMode();
		return;
	}

	/* 存储共享构建状态，为此我们保留了空间 */
	fc_btshared = (BTShared *) shm_toc_allocate(fc_pcxt->toc, fc_estbtshared);
	/* 初始化不可变状态 */
	fc_btshared->heaprelid = RelationGetRelid(fc_btspool->heap);
	fc_btshared->indexrelid = RelationGetRelid(fc_btspool->index);
	fc_btshared->isunique = fc_btspool->isunique;
	fc_btshared->nulls_not_distinct = fc_btspool->nulls_not_distinct;
	fc_btshared->isconcurrent = fc_isconcurrent;
	fc_btshared->scantuplesortstates = fc_scantuplesortstates;
	ConditionVariableInit(&fc_btshared->workersdonecv);
	SpinLockInit(&fc_btshared->mutex);
	/* 初始化可变状态 */
	fc_btshared->nparticipantsdone = 0;
	fc_btshared->reltuples = 0.0;
	fc_btshared->havedead = false;
	fc_btshared->indtuples = 0.0;
	fc_btshared->brokenhotchain = false;
	table_parallelscan_initialize(fc_btspool->heap,
								  ParallelTableScanFromBTShared(fc_btshared),
								  fc_snapshot);

	/*
	 * 存储共享tuplesort-private状态，为此我们保留了空间。
	 * 然后，使用tuplesort例程初始化不透明状态。
	 */
	fc_sharedsort = (Sharedsort *) shm_toc_allocate(fc_pcxt->toc, fc_estsort);
	tuplesort_initialize_shared(fc_sharedsort, fc_scantuplesortstates,
								fc_pcxt->seg);

	shm_toc_insert(fc_pcxt->toc, PARALLEL_KEY_BTREE_SHARED, fc_btshared);
	shm_toc_insert(fc_pcxt->toc, PARALLEL_KEY_TUPLESORT, fc_sharedsort);

	/* 独特情况需要第二个spool，以及相关的共享状态 */
	if (!fc_btspool->isunique)
		fc_sharedsort2 = NULL;
	else
	{
		/*
		 * 存储额外的共享 tuplesort 私有状态，为此我们预留空间。然后，使用 tuplesort 例程初始化不透明状态。
		 */
		fc_sharedsort2 = (Sharedsort *) shm_toc_allocate(fc_pcxt->toc, fc_estsort);
		tuplesort_initialize_shared(fc_sharedsort2, fc_scantuplesortstates,
									fc_pcxt->seg);

		shm_toc_insert(fc_pcxt->toc, PARALLEL_KEY_TUPLESORT_SPOOL2, fc_sharedsort2);
	}

	/* 存储工作进程的查询字符串 */
	if (debug_query_string)
	{
		char	   *fc_sharedquery;

		fc_sharedquery = (char *) shm_toc_allocate(fc_pcxt->toc, fc_querylen + 1);
		memcpy(fc_sharedquery, debug_query_string, fc_querylen + 1);
		shm_toc_insert(fc_pcxt->toc, PARALLEL_KEY_QUERY_TEXT, fc_sharedquery);
	}

	/*
	 * 为每个工作进程的 WalUsage 和 BufferUsage 分配空间；无需初始化。
	 */
	fc_walusage = shm_toc_allocate(fc_pcxt->toc,
								mul_size(sizeof(WalUsage), fc_pcxt->nworkers));
	shm_toc_insert(fc_pcxt->toc, PARALLEL_KEY_WAL_USAGE, fc_walusage);
	fc_bufferusage = shm_toc_allocate(fc_pcxt->toc,
								   mul_size(sizeof(BufferUsage), fc_pcxt->nworkers));
	shm_toc_insert(fc_pcxt->toc, PARALLEL_KEY_BUFFER_USAGE, fc_bufferusage);

	/* 启动工作进程，保存领导者/调用者的状态 */
	LaunchParallelWorkers(fc_pcxt);
	fc_btleader->pcxt = fc_pcxt;
	fc_btleader->nparticipanttuplesorts = fc_pcxt->nworkers_launched;
	if (fc_leaderparticipates)
		fc_btleader->nparticipanttuplesorts++;
	fc_btleader->btshared = fc_btshared;
	fc_btleader->sharedsort = fc_sharedsort;
	fc_btleader->sharedsort2 = fc_sharedsort2;
	fc_btleader->snapshot = fc_snapshot;
	fc_btleader->walusage = fc_walusage;
	fc_btleader->bufferusage = fc_bufferusage;

	/* 如果没有成功启动工作进程，则回退（执行串行构建） */
	if (fc_pcxt->nworkers_launched == 0)
	{
		fc__bt_end_parallel(fc_btleader);
		return;
	}

	/* 现在保存领导者状态，因为构建将是并行的 */
	fc_buildstate->btleader = fc_btleader;

	/* 自己加入堆扫描 */
	if (fc_leaderparticipates)
		fc__bt_leader_participate_as_worker(fc_buildstate);

	/*
	 * 调用者需要在我们返回时等待所有启动的工作进程。确保失败启动的情况不会永远挂起。
	 */
	WaitForParallelWorkersToAttach(fc_pcxt);
}

/*
 * 关闭工作进程，销毁并行上下文，并结束并行模式。
 */
static void fc__bt_end_parallel(BTLeader *fc_btleader)
{
	int			fc_i;

	/* 关闭工作进程 */
	WaitForParallelWorkersToFinish(fc_btleader->pcxt);

	/*
	 * 接下来，累积 WAL 使用情况。（这必须等待工作进程完成，否则我们可能会得到不完整的数据。）
	 */
	for (fc_i = 0; fc_i < fc_btleader->pcxt->nworkers_launched; fc_i++)
		InstrAccumParallelQuery(&fc_btleader->bufferusage[fc_i], &fc_btleader->walusage[fc_i]);

	/* 释放对 MVCC 快照的最后引用（如果使用了） */
	if (IsMVCCSnapshot(fc_btleader->snapshot))
		UnregisterSnapshot(fc_btleader->snapshot);
	DestroyParallelContext(fc_btleader->pcxt);
	ExitParallelMode();
}

/*
 * 返回根据其并行扫描将使用的快照存储并行 btree 索引构建所需的共享内存大小。
 */
static Size fc__bt_parallel_estimate_shared(Relation fc_heap, Snapshot fc_snapshot)
{
	/* 参见 shm_toc_allocate 了解为何使用 BUFFERALIGN */
	return add_size(BUFFERALIGN(sizeof(BTShared)),
					table_parallelscan_estimate(fc_heap, fc_snapshot));
}

/*
 * 在领导者内，等待堆扫描结束。
 *
 * 当调用时，由 _bt_begin_parallel() 启动的并行堆扫描将在工作进程中进行（当领导者作为工作进程参与时，我们应该在这里正好当工作进程完成）。
 *
 * 填充 ambuild 统计所需的字段，并允许调用者设置字段以指示某个工作进程遇到了损坏的 HOT 链。
 *
 * 返回扫描的堆元组总数。
 */
static double fc__bt_parallel_heapscan(BTBuildState *fc_buildstate, bool *fc_brokenhotchain)
{
	BTShared   *fc_btshared = fc_buildstate->btleader->btshared;
	int			fc_nparticipanttuplesorts;
	double		fc_reltuples;

	fc_nparticipanttuplesorts = fc_buildstate->btleader->nparticipanttuplesorts;
	for (;;)
	{
		SpinLockAcquire(&fc_btshared->mutex);
		if (fc_btshared->nparticipantsdone == fc_nparticipanttuplesorts)
		{
			fc_buildstate->havedead = fc_btshared->havedead;
			fc_buildstate->indtuples = fc_btshared->indtuples;
			*fc_brokenhotchain = fc_btshared->brokenhotchain;
			fc_reltuples = fc_btshared->reltuples;
			SpinLockRelease(&fc_btshared->mutex);
			break;
		}
		SpinLockRelease(&fc_btshared->mutex);

		ConditionVariableSleep(&fc_btshared->workersdonecv,
							   WAIT_EVENT_PARALLEL_CREATE_INDEX_SCAN);
	}

	ConditionVariableCancelSleep();

	return fc_reltuples;
}

/*
 * 在领导者内，作为并行工作进程参与。
 */
static void fc__bt_leader_participate_as_worker(BTBuildState *fc_buildstate)
{
	BTLeader   *fc_btleader = fc_buildstate->btleader;
	BTSpool    *fc_leaderworker;
	BTSpool    *fc_leaderworker2;
	int			fc_sortmem;

	/* 分配内存并初始化私有缓冲区 */
	fc_leaderworker = (BTSpool *) palloc0(sizeof(BTSpool));
	fc_leaderworker->heap = fc_buildstate->spool->heap;
	fc_leaderworker->index = fc_buildstate->spool->index;
	fc_leaderworker->isunique = fc_buildstate->spool->isunique;
	fc_leaderworker->nulls_not_distinct = fc_buildstate->spool->nulls_not_distinct;

	/* 如果需要，初始化第二个缓冲区 */
	if (!fc_btleader->btshared->isunique)
		fc_leaderworker2 = NULL;
	else
	{
		/* 为工作进程自己的私有辅助缓冲区分配内存 */
		fc_leaderworker2 = (BTSpool *) palloc0(sizeof(BTSpool));

		/* 初始化工作进程自己的辅助缓冲区 */
		fc_leaderworker2->heap = fc_leaderworker->heap;
		fc_leaderworker2->index = fc_leaderworker->index;
		fc_leaderworker2->isunique = false;
	}

	/*
	 * 既然在分配 maintenance_work_mem 时不妨使用可靠的数字（在请求的工作进程未被启动时，这会比其他工作进程略高）。
	 */
	fc_sortmem = maintenance_work_mem / fc_btleader->nparticipanttuplesorts;

	/* 执行所有参与者的共同工作 */
	fc__bt_parallel_scan_and_sort(fc_leaderworker, fc_leaderworker2, fc_btleader->btshared,
							   fc_btleader->sharedsort, fc_btleader->sharedsort2,
							   fc_sortmem, true);

#ifdef BTREE_BUILD_STATS
	if (log_btree_build_stats)
	{
		ShowUsage("BTREE BUILD (Leader Partial Spool) STATISTICS");
		ResetUsage();
	}
#endif							/* BTREE_BUILD_STATS */
}

/*
 * 在启动的并行进程中执行工作。
 */
void _bt_parallel_build_main(dsm_segment *fc_seg, shm_toc *fc_toc)
{
	char	   *fc_sharedquery;
	BTSpool    *fc_btspool;
	BTSpool    *fc_btspool2;
	BTShared   *fc_btshared;
	Sharedsort *fc_sharedsort;
	Sharedsort *fc_sharedsort2;
	Relation	fc_heapRel;
	Relation	fc_indexRel;
	LOCKMODE	fc_heapLockmode;
	LOCKMODE	fc_indexLockmode;
	WalUsage   *fc_walusage;
	BufferUsage *fc_bufferusage;
	int			fc_sortmem;

#ifdef BTREE_BUILD_STATS
	if (log_btree_build_stats)
		ResetUsage();
#endif							/* BTREE_BUILD_STATS */

	/*
	 * 唯一可以设置为并行工作进程的状态标志是 PROC_IN_SAFE_IC。
	 */
	Assert((MyProc->statusFlags == 0) ||
		   (MyProc->statusFlags == PROC_IN_SAFE_IC));

	/* 首先为单个工作进程设置 debug_query_string */
	fc_sharedquery = shm_toc_lookup(fc_toc, PARALLEL_KEY_QUERY_TEXT, true);
	debug_query_string = fc_sharedquery;

	/* 报告领导者的查询字符串 */
	pgstat_report_activity(STATE_RUNNING, debug_query_string);

	/* 查找 nbtree 共享状态 */
	fc_btshared = shm_toc_lookup(fc_toc, PARALLEL_KEY_BTREE_SHARED, false);

	/* 使用 index.c 已知的锁模式打开关系 */
	if (!fc_btshared->isconcurrent)
	{
		fc_heapLockmode = ShareLock;
		fc_indexLockmode = AccessExclusiveLock;
	}
	else
	{
		fc_heapLockmode = ShareUpdateExclusiveLock;
		fc_indexLockmode = RowExclusiveLock;
	}

	/* 在工作进程内打开关系 */
	fc_heapRel = table_open(fc_btshared->heaprelid, fc_heapLockmode);
	fc_indexRel = index_open(fc_btshared->indexrelid, fc_indexLockmode);

	/* 初始化工作进程自己的缓冲区 */
	fc_btspool = (BTSpool *) palloc0(sizeof(BTSpool));
	fc_btspool->heap = fc_heapRel;
	fc_btspool->index = fc_indexRel;
	fc_btspool->isunique = fc_btshared->isunique;
	fc_btspool->nulls_not_distinct = fc_btshared->nulls_not_distinct;

	/* 查找 tuplesort.c 私有的共享状态 */
	fc_sharedsort = shm_toc_lookup(fc_toc, PARALLEL_KEY_TUPLESORT, false);
	tuplesort_attach_shared(fc_sharedsort, fc_seg);
	if (!fc_btshared->isunique)
	{
		fc_btspool2 = NULL;
		fc_sharedsort2 = NULL;
	}
	else
	{
		/* 为工作进程自己的私有辅助缓冲区分配内存 */
		fc_btspool2 = (BTSpool *) palloc0(sizeof(BTSpool));

		/* 初始化工作进程自己的辅助缓冲区 */
		fc_btspool2->heap = fc_btspool->heap;
		fc_btspool2->index = fc_btspool->index;
		fc_btspool2->isunique = false;
		/* 查找 tuplesort.c 私有的共享状态 */
		fc_sharedsort2 = shm_toc_lookup(fc_toc, PARALLEL_KEY_TUPLESORT_SPOOL2, false);
		tuplesort_attach_shared(fc_sharedsort2, fc_seg);
	}

	/* 准备在并行执行期间跟踪缓冲区使用情况 */
	InstrStartParallelQuery();

	/* 执行对 spool 的排序，可能还包括 spool2 */
	fc_sortmem = maintenance_work_mem / fc_btshared->scantuplesortstates;
	fc__bt_parallel_scan_and_sort(fc_btspool, fc_btspool2, fc_btshared, fc_sharedsort,
							   fc_sharedsort2, fc_sortmem, false);

	/* 在并行执行期间报告 WAL/缓冲区使用情况 */
	fc_bufferusage = shm_toc_lookup(fc_toc, PARALLEL_KEY_BUFFER_USAGE, false);
	fc_walusage = shm_toc_lookup(fc_toc, PARALLEL_KEY_WAL_USAGE, false);
	InstrEndParallelQuery(&fc_bufferusage[ParallelWorkerNumber],
						  &fc_walusage[ParallelWorkerNumber]);

#ifdef BTREE_BUILD_STATS
	if (log_btree_build_stats)
	{
		ShowUsage("BTREE BUILD (Worker Partial Spool) STATISTICS");
		ResetUsage();
	}
#endif							/* BTREE_BUILD_STATS */

	index_close(fc_indexRel, fc_indexLockmode);
	table_close(fc_heapRel, fc_heapLockmode);
}

/*
 * 执行并行排序的工作部分。
 *
 * 这会为传入的 btspool 生成一个 tuplesort，如果需要第二个 btspool
 * （即用于唯一索引构建），则还会生成第二个 tuplesort 状态。所有
 * 其他 spool 字段在调用此函数时应已设置。
 *
 * sortmem 是每个工作者使用的工作内存量,
 * 以 KB 为单位表示。
 *
 * 当此函数返回时，工作者已完成，只需释放资源。
 */
static void fc__bt_parallel_scan_and_sort(BTSpool *fc_btspool, BTSpool *fc_btspool2,
						   BTShared *fc_btshared, Sharedsort *fc_sharedsort,
						   Sharedsort *fc_sharedsort2, int fc_sortmem, bool fc_progress)
{
	SortCoordinate fc_coordinate;
	BTBuildState fc_buildstate;
	TableScanDesc fc_scan;
	double		fc_reltuples;
	IndexInfo  *fc_indexInfo;

	/* 初始化本地 tuplesort 协调状态 */
	fc_coordinate = palloc0(sizeof(SortCoordinateData));
	fc_coordinate->isWorker = true;
	fc_coordinate->nParticipants = -1;
	fc_coordinate->sharedsort = fc_sharedsort;

	/* 开始 "部分" tuplesort */
	fc_btspool->sortstate = tuplesort_begin_index_btree(fc_btspool->heap,
													 fc_btspool->index,
													 fc_btspool->isunique,
													 fc_btspool->nulls_not_distinct,
													 fc_sortmem, fc_coordinate,
													 TUPLESORT_NONE);

	/*
	 * 正如与串行案例一样，可能会有第二个 spool。如果是这样，则需要
	 * 第二个专用的 spool2 部分 tuplesort。
	 */
	if (fc_btspool2)
	{
		SortCoordinate fc_coordinate2;

		/*
		 * 我们预计第二个（用于死元组）不会很满，
		 * 所以只给它工作内存（除非工作者的 sortmem 更少）。工作进程
		 * 通常被允许独立分配工作内存。
		 */
		fc_coordinate2 = palloc0(sizeof(SortCoordinateData));
		fc_coordinate2->isWorker = true;
		fc_coordinate2->nParticipants = -1;
		fc_coordinate2->sharedsort = fc_sharedsort2;
		fc_btspool2->sortstate =
			tuplesort_begin_index_btree(fc_btspool->heap, fc_btspool->index, false, false,
										Min(fc_sortmem, work_mem), fc_coordinate2,
										false);
	}

	/* 填充 _bt_build_callback() 的 buildstate */
	fc_buildstate.isunique = fc_btshared->isunique;
	fc_buildstate.nulls_not_distinct = fc_btshared->nulls_not_distinct;
	fc_buildstate.havedead = false;
	fc_buildstate.heap = fc_btspool->heap;
	fc_buildstate.spool = fc_btspool;
	fc_buildstate.spool2 = fc_btspool2;
	fc_buildstate.indtuples = 0;
	fc_buildstate.btleader = NULL;

	/* 合并并行扫描 */
	fc_indexInfo = BuildIndexInfo(fc_btspool->index);
	fc_indexInfo->ii_Concurrent = fc_btshared->isconcurrent;
	fc_scan = table_beginscan_parallel(fc_btspool->heap,
									ParallelTableScanFromBTShared(fc_btshared));
	fc_reltuples = table_index_build_scan(fc_btspool->heap, fc_btspool->index, fc_indexInfo,
									   true, fc_progress, fc__bt_build_callback,
									   (void *) &fc_buildstate, fc_scan);

	/* 执行该工作者的排序部分 */
	if (fc_progress)
		pgstat_progress_update_param(PROGRESS_CREATEIDX_SUBPHASE,
									 PROGRESS_BTREE_PHASE_PERFORMSORT_1);
	tuplesort_performsort(fc_btspool->sortstate);
	if (fc_btspool2)
	{
		if (fc_progress)
			pgstat_progress_update_param(PROGRESS_CREATEIDX_SUBPHASE,
										 PROGRESS_BTREE_PHASE_PERFORMSORT_2);
		tuplesort_performsort(fc_btspool2->sortstate);
	}

	/*
	 * 完成。记录 ambuild 统计信息，以及我们是否遇到断开的
	 * HOT 链。
	 */
	SpinLockAcquire(&fc_btshared->mutex);
	fc_btshared->nparticipantsdone++;
	fc_btshared->reltuples += fc_reltuples;
	if (fc_buildstate.havedead)
		fc_btshared->havedead = true;
	fc_btshared->indtuples += fc_buildstate.indtuples;
	if (fc_indexInfo->ii_BrokenHotChain)
		fc_btshared->brokenhotchain = true;
	SpinLockRelease(&fc_btshared->mutex);

	/* 通知领导者 */
	ConditionVariableSignal(&fc_btshared->workersdonecv);

	/* 我们可以立即结束 tuplesorts */
	tuplesort_end(fc_btspool->sortstate);
	if (fc_btspool2)
		tuplesort_end(fc_btspool2->sortstate);
}
